/*
 *******************************************************************
 * Author: xuelei
 * Create Date: 2021-09-09 14:13
 * Description: create file
 *******************************************************************
 */
package com.xuelei.cloud.bc.crypto.symmetric;

import com.xuelei.cloud.bc.Checker;
import com.xuelei.cloud.bc.Provider;
import com.xuelei.cloud.bc.crypto.exception.CryptoException;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

/**
 * @author xuelei
 * @date 2021-09-09
 */
public class SM4Util {
    
    private static final Logger log = LoggerFactory.getLogger(SM4Util.class);
    
    private static final String SM4 = "SM4";
    
    private static final String SM4_ECB_PKCS5 = "SM4/ECB/PKCS5Padding";
    
    private static final int KEY_SIZE = 128;
    
    private static final String SECURITY_RANDOM_ALGORITHM = "SHA1PRNG";
    
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    
    private static final int ENCRYPT = Cipher.ENCRYPT_MODE;
    
    private static final int DECRYPT = Cipher.DECRYPT_MODE;
    
    private static SecureRandom secureRandom;
    
    static {
        try {
            secureRandom = SecureRandom.getInstance(SECURITY_RANDOM_ALGORITHM);
            secureRandom.setSeed(secureRandom.generateSeed(16));
        } catch (Exception e) {
            log.error("Exception: {}, message: {}", e.getClass(), e.getMessage());
        }
    }
    
    /**
     * 数据块加密
     * @param plainBlock 原始数据块
     * @param keyBase16 Base16编码的密钥
     * @return 加密后的数据块
     * @throws CryptoException
     */
    public static byte[] encrypt(byte[] plainBlock, String keyBase16) throws CryptoException {
        byte[] cipherBlock = null;
        try {
            Checker.checkBlock(plainBlock, "plain block is empty");
            Checker.checkText(keyBase16, "key is empty");
            Cipher cipher = SM4Util.createSm4Cipher(ENCRYPT, keyBase16);
            cipherBlock = cipher.doFinal(plainBlock);
        } catch (NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException
                                     | NoSuchAlgorithmException | InvalidKeyException e) {
            log.error("Exception: {}, message: {}", e.getClass(), e.getMessage());
            throw new CryptoException(e.getMessage());
        }
        return cipherBlock;
    }
    
    /**
     * 字符串加密
     * @param plainText 字符串明文
     * @param keyBase16 Base16编码的key
     * @return Base16编码的密文
     * @throws CryptoException
     */
    public static String encrypt(String plainText, String keyBase16) throws CryptoException {
        Checker.checkText(plainText, "plain text is empty");
        Checker.checkText(keyBase16, "key is empty");
        byte[] plainBlock = plainText.getBytes(DEFAULT_CHARSET);
        byte[] cipherBlock = SM4Util.encrypt(plainBlock, keyBase16);
        return Hex.toHexString(cipherBlock);
    }
    
    /**
     * 数据块解密
     * @param cipherBlock 被加密的数据块
     * @param keyBase16 Base16编码的key
     * @return 解密后的数据块
     * @throws CryptoException
     */
    public static byte[] decrypt(byte[] cipherBlock, String keyBase16) throws CryptoException {
        byte[] plainBlock = null;
        try {
            Checker.checkBlock(cipherBlock, "cipher block is empty");
            Checker.checkText(keyBase16, "key is empty");
            Cipher cipher = SM4Util.createSm4Cipher(DECRYPT, keyBase16);
            plainBlock = cipher.doFinal(cipherBlock);
        } catch (NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException
                                         | BadPaddingException | InvalidKeyException e) {
            log.error("Exception: {}, message: {}", e.getClass(), e.getMessage());
            throw new CryptoException(e.getMessage());
        }
        return plainBlock;
    }
    
    /**
     * 字符串解密
     * @param cipherTextBase16 Base16编码的密文
     * @param keyBase16 Base16编码的key
     * @return 字符串明文
     * @throws CryptoException
     */
    public static String decrypt(String cipherTextBase16, String keyBase16) throws CryptoException {
        Checker.checkText(cipherTextBase16, "cipher text is empty");
        Checker.checkText(keyBase16, "key is empty");
        byte[] cipherBlock = Hex.decode(cipherTextBase16);
        byte[] bytes = SM4Util.decrypt(cipherBlock, keyBase16);
        return new String(bytes);
    }
    
    /**
     * 创建一个用于加密和解密的密钥
     * @return Base16编码的密钥
     * @throws CryptoException
     */
    public static String createSecretKeyEncodedHex() throws CryptoException {
        SecretKey secretKey = SM4Util.createSecretKey();
        return Hex.toHexString(secretKey.getEncoded());
    }
    
    private static Cipher createSm4Cipher(int CipherMode, String keyEncodedHex) throws NoSuchPaddingException,
                                                                                       NoSuchAlgorithmException,
                                                                                       InvalidKeyException {
        Cipher cipher = Cipher.getInstance(SM4_ECB_PKCS5, Provider.BouncyCastle);
        SecretKey secretKey = SM4Util.buildSecreteKey(keyEncodedHex);
        cipher.init(CipherMode, secretKey, secureRandom);
        return cipher;
    }
    
    private static SecretKey buildSecreteKey(String keyEncodedHex) {
        return new SecretKeySpec(Hex.decode(keyEncodedHex), SM4);
    }
    
    private static SecretKey createSecretKey() throws CryptoException {
        KeyGenerator keyGenerator = null;
        try {
            keyGenerator = KeyGenerator.getInstance(SM4, Provider.BouncyCastle);
        } catch (NoSuchAlgorithmException e) {
            log.error("Exception: {}, message: {}", e.getClass(), e.getMessage());
            throw new CryptoException(e.getMessage());
        }
        keyGenerator.init(KEY_SIZE, secureRandom);
        return keyGenerator.generateKey();
    }
}
